#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Shakira is a simple Python IRC bot, based on a trivial Markov chain code
# Copyright (C) 2009 - Sharayanan <sharayanan@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import sys
import random
import socket 


TERM_HEADER = """
Shakira Copyright (C) 2009  Sharayanan
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions; see COPYING for more information.
"""

class irc_interface():
    '''
        Main class for IRC connection handling and message parsing
        and sending
    '''
    default_options = {
      'host' : 'irc.somewhere.org',
      'port' : 6667,
      'nick' : 'Shakira',
      'ident': 'Shakira',
      'realname' : 'Shakira',
      'owner' : 'myowner',
      'channel' : '#seomchan',
      'verbose' : False,
      'buffer_size': 512,
    }

    def __init__(self, **kwargs):
        '''
            Constructor
        '''
        self.options = dict(irc_interface.default_options)
        self.options.update(kwargs)

        self.readbuffer = ''                         # Read buffer
        self.interpreter = None                      # Lexical interpreter

        self.expression = {}
        file = open('expressions.txt', 'r')
        # DANGEROUS
        result = file.read()
        exec('self.expressions = ' + result)

        if self.options['verbose']: print("[INFO] IRC Interface loaded")

    def __getitem__(self, key):
      return self.options[key]

    def s_send(self, data):
        '''
            Send raw data to the server
        '''
        self.s.send(str.encode(data))
        
    def attach(self, interpreter):
        '''
            Attach lexical engine to the connection
        '''
        self.interpreter = interpreter
        
    def connect(self):
        '''
            Setup connection with remote server
        '''
        
        # Connection to the server
        try:
          self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
          self.s.connect((self.options['host'], self.options['port']))
        except:
          print('[ERROR] Cannot connect to the IRC server %s:%s'%(self.options['host'], self.options['port']))
          sys.exit(1)
        
        # Sending nickname information
        self.s_send('NICK '+ self.options['nick'] + '\n')
        self.s_send('USER '+ self.options['ident'] + ' '+ self.options['host'] + ' bla :'+ self.options['realname'] + '\n')

    def join(self, callback):
        '''
            Join a channel and begin conversation
	    Note : this is *ugly*
        '''
        
        # Join channel
        self.s_send('JOIN '+ self.options['channel'] + '\n')
		
        while 1:
            '''
                Main loop
            '''
            
            # Receive data and split colons
            line = self.s.recv(self.options['buffer_size'])
            splitted = line.decode('latin-1').lower().split(":")
            
            # A valid message has at least two parts
            if (len(splitted) >= 2):
            
                # Kludge to avoid misinterpreting smileys and other :P-like elements
                message = ":".join(splitted[2:])
                if self.options['verbose']: print ("[PARSING] %s"%(repr(message)))               
 
                try:
                    # Extract message metainformation
                    (author, type, dest) = splitted[1].strip(" ").split(" ")
                    author = author.strip(":").split("!")[0]
                    
                    if self.options['verbose']: print('[INPUT] %s'%message)

                    if (message in self.expressions):
                      self.speak(random.choice(self.expressions[message]))
                      type = ''

                    # Unimplemented : obey the master
                    if ((author == self.options['owner']) and (dest == self.options['nick'])):
                        pass
                    
                    # In case the message is directed to the bot
                    if (type == 'privmsg'):
                        if ((message.lower()[:len(self.options['nick'])] == self.options['nick'].lower()) or (dest == self.options['nick'])):
                            # Answer to the author
                            response = callback(message.lower().replace(self.options['nick'].lower()+':', ''), self.interpreter)

                            if (response.strip() != ''):
                                response = author + ': ' + response
                                self.speak(response)
                            else:
                                if self.options['verbose']: print('[WARNING] Nothing to say...')
                        
                except:
                    # In case we're PINGed, PONG back
                    if (splitted[0] == 'ping '):
                        self.s_send('PONG :' + splitted[1])
                        if self.options['verbose']: print('[INFO] Ping/pong')
                    else:
                        print('[WARNING] Unhandled message : ' + repr(splitted))

    def speak(self, text):
        '''
            Send a PRIVSMG on the channel
        '''
        if self.options['verbose']: print ("[OUTPUT] Sent : %s"%(text))
        self.s_send('PRIVMSG ' + self.options['channel'] + ' ' + text + '\n')

class lexical_engine():
    '''
        Lexical engine class
        Based on Markov chains
    '''
    
    def __init__(self):
        '''
            Constructor
        '''
        self.end_marks = ['.', '!', '?']        # End of sentence punctuation
        self.begin_sentence = []                # Elements that begin a sentence
        self.word_dict = {}                     # Known vocabulary
        self.max_len = 300                      # Maximum length of a message
        self.file_name = 'data.txt'          # Name of the file to save data to
     
    def save_to_disk(self):
        '''
            Saves the lexical engine to a file
        '''
        result = ''
        
        # Save end-of-sentence punctuation
        result += "@SECTION"
        result += repr(self.end_marks)
        
        # Save sentence stems
        result += "@SECTION"
        result += repr(self.begin_sentence)
        
        # Save vocabulary
        result += "@SECTION"
        result += repr(self.word_dict)
        
        # Write file
        file = open(self.file_name, 'w')
        file.write(result)
        file.close()
     
    def load_from_disk(self):
        '''
            Load lexical engine from disk
            FIXME : crashes if file is missing
        '''
        
        # Load file from disk
        file = open(self.file_name, 'r')
        result = file.read()
        file.close()
        
        parts = result.split('@SECTION')
        
        try:
            # DANGEROUS : exec directly data
            exec('self.end_marks = '       + parts[1])
            exec('self.begin_sentence = '  + parts[2])
            exec('self.word_dict = '       + parts[3])
        
            print('[INFO] ' + str(len(parts[2])) + ' words loaded')
        except:
            print('[WARNING] No file loaded, or file invalid')
     
    def analyze_sentence(self, text = ''):
        '''
            Analyze and prepare sentences
        '''
        
        # Don't allow empty strings
        if text == '': return
    
        # Preparing result
        text = ' ' + text + ' '
        position = 0
        
        # Splitting words
        words = text.lower().split()
        
        # Save sentence stems
        if len(words) < 3 :
            
            begin_sentence = ('', '', '')
            if len(words) == 1 : begin_sentence = ('', words[0])
            if len(words) == 2 : begin_sentence = (words[0], words[1])
            
            if not (begin_sentence in self.begin_sentence): self.begin_sentence.append(begin_sentence)            
            return        

        begin_sentence = (words[0], words[1])
        if not (begin_sentence in self.begin_sentence): self.begin_sentence.append(begin_sentence)
        
        # Offset position to account for several sentences
        position = len(words[0]) + len(words[1]) + 3
        
        # Parse each word
        for word_index in range(2, len(words)):
        
            position += 1 + len(words[word_index])
        
            # Get preceding words
            prev1 = words[word_index - 2]
            prev2 = words[word_index - 1]
            
            key = (prev1, prev2)
            
            # Build Markov chain
            if key in self.word_dict.keys():
                if not (words[word_index] in self.word_dict[key]):
                    self.word_dict[key].append(words[word_index])
                else:
                    print ("[INFO] Ignoring %s > %s"%(key, words[word_index]))
            else:
                self.word_dict[key] = [words[word_index]]
                
            if words[word_index][-1:] in self.end_marks:
                self.analyze_sentence(text[position:])
                self.save_to_disk()
                return

        # Autosave
        self.save_to_disk()
        
    def output_sentence(self, count = 1):
        '''
            Build a sentence from known words
        '''
        
        # Choose a random stem
        result = ''
        key = random.choice(self.begin_sentence)
        result += key[0] + ' '
        
        # Add random followers to build up the sentence
        while (key in self.word_dict.keys()) and (len(result) < self.max_len):
            result += key[1] + ' '
            key = (key[1], random.choice(self.word_dict[key]))

        result += key[1] + ' '
            
        # Output sentence
        return result

def parse_message(data, interpreter):
    '''
        Callback function to interpret and output sentences
    '''
    interpreter.analyze_sentence(data)
    result = interpreter.output_sentence()
    
    return result
        
if __name__ == '__main__':
    '''
        Bootstrap
    '''

    print(TERM_HEADER)
    
    # Retrieve command line arguments
    arguments = sys.argv[1:]

    bot_online  = ('-online' in arguments)
    bot_verbose = ('-v' in arguments)

    if bot_verbose: print("Running with arguments : %s"% arguments)

    # Load lexical engine
    lex_eng = lexical_engine()
    lex_eng.load_from_disk()
    
    if bot_online:
      # Online mode - connect to IRC
      irc_itf = irc_interface(verbose = bot_verbose)
      irc_itf.connect()
      irc_itf.attach(lex_eng)
      irc_itf.join(parse_message)
    else: 
      # Offline mode (mainly for debugging)
      #TODO: use common code for offline/online message handling
      if bot_verbose: print("[INFO] Running offline in *reduced* mode (for full-featured connected mode, use -online)")
      while 1:
        lex_eng.analyze_sentence(input("> "))
        print(lex_eng.output_sentence())
